package org.unidata.mdm.rest.v1.meta.service.entities;

import static org.unidata.mdm.rest.v1.meta.converter.entities.EntityDefinitionConverter.toRegisterEntityRO;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;

import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.meta.context.GetDataModelContext;
import org.unidata.mdm.meta.context.UpsertDataModelContext;
import org.unidata.mdm.meta.dto.GetEntityDTO;
import org.unidata.mdm.meta.dto.GetModelDTO;
import org.unidata.mdm.rest.v1.meta.converter.RelationDefinitionConverter;
import org.unidata.mdm.rest.v1.meta.converter.entities.EntityDefinitionConverter;
import org.unidata.mdm.rest.v1.meta.exception.MetaRestExceptionIds;
import org.unidata.mdm.rest.v1.meta.ro.entities.RegisterEntityRO;
import org.unidata.mdm.rest.v1.meta.ro.entities.register.DeleteRegisterResultRO;
import org.unidata.mdm.rest.v1.meta.ro.entities.register.GetRegisterResultRO;
import org.unidata.mdm.rest.v1.meta.ro.entities.register.GetRegistersResultRO;
import org.unidata.mdm.rest.v1.meta.ro.entities.register.UpsertRegisterRequestRO;
import org.unidata.mdm.rest.v1.meta.ro.entities.register.UpsertRegisterResultRO;
import org.unidata.mdm.system.exception.PlatformBusinessException;
import org.unidata.mdm.system.service.TouchService;
import org.unidata.mdm.system.type.touch.Touch;
import org.unidata.mdm.system.type.touch.TouchParams;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;

/**
 * Register entities rest controller
 *
 * @author Alexandr Serov
 * @since 23.11.2020
 **/
@Path(RegisterEntityRestService.SERVICE_PATH)
@Consumes({"application/json"})
@Produces({"application/json"})
public class RegisterEntityRestService extends AbstractMetaEntitiesRestService {

    public static final String SERVICE_PATH = "register-entities";

    public static final String SERVICE_TAG = "register-entities";

    private static final String NOT_FOUND_ERROR = "Register %s not found (draftId: %s)";

    /**
     * Has register data.
     */
    private static final Touch<Boolean> TOUCH_HAS_REGISTER_DATA
        = Touch.builder(Boolean.class)
            .touchName("[touch-has-register-data]")
            .paramType("register-name", String.class)
            .build();
    /**
     * The TS.
     */
    @Autowired
    private TouchService touchService;

    @GET
    @Path("{id}")
    @Operation(description = "Gets a register by ID.", method = HttpMethod.GET, tags = SERVICE_TAG)
    public GetRegisterResultRO findByName(@Parameter(description = "ID.", in = ParameterIn.PATH)
                                          @PathParam("id") String id,
                                          @Parameter(description = "Draft ID. Optional.", in = ParameterIn.QUERY)
                                          @QueryParam("draftId") @DefaultValue("0") Long draftId,
                                          @Parameter(description = "Check for data existance.", in = ParameterIn.QUERY)
                                          @QueryParam("checkData") @DefaultValue("true") Boolean checkData) {

        GetRegisterResultRO result = new GetRegisterResultRO();
        if (BooleanUtils.isTrue(checkData)) {
            result.setRegisterEntity(dataIndicator(toRegisterEntityRO(registerByName(id, draftId))));
        } else {
            GetEntityDTO dto = findRegisterByName(id, draftId);
            if (dto != null && Objects.nonNull(dto.getEntity())) {
                result.setRegisterEntity(toRegisterEntityRO(dto));
            }
        }
        return result;
    }

    /**
     * Gets a list of register entities.
     *
     * @param draftId draftId
     * @return list of entity info
     */
    @GET
    @Operation(
        description = "Register list.",
        method = HttpMethod.GET,
        tags = SERVICE_TAG)
    public GetRegistersResultRO findAll(
            @Parameter(description = "Draft id. Optional.", in = ParameterIn.QUERY) @QueryParam("draftId") @DefaultValue("0") Long draftId,
            @Parameter(description = "Check for data existance.", in = ParameterIn.QUERY) @QueryParam("checkData") @DefaultValue("true") Boolean checkData) {
        GetRegistersResultRO result = new GetRegistersResultRO();
        List<GetEntityDTO> lookups = findEntitiesByDraft(draftId, GetModelDTO::getEntities);
        if (!lookups.isEmpty()) {
            result.setRegisterEntities(lookups.stream()
                .map(EntityDefinitionConverter::toRegisterEntityRO)
                .filter(el -> allow(el.getName()))
                .map(ro -> checkData.booleanValue() ? dataIndicator(ro) : ro)
                .collect(Collectors.toList()));
        } else {
            result.setRegisterEntities(Collections.emptyList());
        }
        return result;
    }


    @POST
    @Path("upsert")
    @Operation(
        description = "Create or update a register.",
        method = HttpMethod.PUT,
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = UpsertRegisterRequestRO.class)), description = "Upsert request."),
        tags = SERVICE_TAG
    )
    public UpsertRegisterResultRO upsert(UpsertRegisterRequestRO req) {
        Objects.requireNonNull(req, "Request can't be null");
        UpsertRegisterResultRO result = new UpsertRegisterResultRO();
        RegisterEntityRO src = notNull("registerEntity", req.getRegisterEntity());
        Long draftId = resolveDraftId(req.getDraftId());
        metaModelService.upsert(UpsertDataModelContext.builder()
            .entitiesUpdate(EntityDefinitionConverter.toEntity(src))
            .relationsUpdate(RelationDefinitionConverter.convert(src.getRelations()))
            .draftId(draftId)
            .build());
        req.setDraftId(draftId);
        result.setDraftId(draftId);
        return result;
    }

    /**
     * Delete lookup entity.
     *
     * @param id id to delete
     * @return 200 Ok
     */
    @DELETE
    @Path("{id}")
    @Operation(
        description = "Removes a register.",
        method = HttpMethod.DELETE,
        parameters = {
            @Parameter(description = "Register id.", in = ParameterIn.PATH, name = "id"),
            @Parameter(description = "Existing draft id. Optional.", in = ParameterIn.QUERY, name = "draftId")
        }, tags = SERVICE_TAG
    )
    public DeleteRegisterResultRO delete(@PathParam("id") String id, @QueryParam("draftId") @DefaultValue("0") Long draftId) {
        DeleteRegisterResultRO result = new DeleteRegisterResultRO();
        GetEntityDTO dto = registerByName(id, draftId);
        metaModelService.upsert(UpsertDataModelContext.builder()
            .entitiesDelete(id)
            .draftId(resolveDraftId(draftId))
            .build());
        result.setId(id);
        return result;
    }

    private GetEntityDTO registerByName(String entityName, Long draftId) {
        GetEntityDTO result = findRegisterByName(entityName, draftId);
        if (result == null) {
            throw new PlatformBusinessException(String.format(NOT_FOUND_ERROR, entityName, draftId), MetaRestExceptionIds.EX_META_DATA_ENTITY_NOT_FOUND);
        }
        return result;
    }

    private GetEntityDTO findRegisterByName(String entityName, Long draftId) {
        return findFirstEntityByRequest(GetDataModelContext.builder()
            .entityIds(Collections.singletonList(entityName))
            .draftId(resolveDraftId(draftId))
            .build(), GetModelDTO::getEntities);
    }

    private <T> List<T> findEntitiesByDraft(Long draftId, Function<GetModelDTO, List<T>> mapper) {
        return findEntitiesByRequest(GetDataModelContext.builder()
                .draftId(resolveDraftId(draftId))
                .allEntities(true)
                .build(), mapper);
    }

    private RegisterEntityRO dataIndicator(RegisterEntityRO ro) {

        if (Objects.nonNull(ro)) {

            ro.setHasData(BooleanUtils.toBoolean(
                    touchService.singleTouch(new TouchParams<>(TOUCH_HAS_REGISTER_DATA)
                            .with("register-name", ro.getName()))));
        }

        return ro;
    }
}
